# -*- coding: utf-8 -*-
"""
UTc! module: src.communication

Purpose
=======
 Provides routines for synchronously and asynchronously communicating with a
 UTc! client.
 
Legal
=====
 All code, unless otherwise indicated, is original, and subject to the
 terms of the GPLv2, which is provided in COPYING.
 
 (C) Neil Tallim, 2009
"""
import select
import socket
import threading
import time

_PACKET_SIZE = 8192 #: The maximum amount of data to try to read from the server.
_TIMEOUT = 10.0 #: The number of seconds to wait before considering the connection broken.

class ServerSocket(threading.Thread):
	"""
	A class that accepts and delegates connections received from clients.
	"""
	_alive = True #: False when this server is no longer needed.
	_server = None #: The socket on which to listen for connections.
	_spawn_args = None #: The arguments to pass to the constructor of the spawned class, as a tuple.
	_spawn_thread = None #: The class to instantiate when a connection is received.
	
	def __init__(self, port, spawn_thread, spawn_args):
		"""
		Initialises all variables needed to manage this server, then begins
		listening for connections.
		
		@type port: int
		@param port: The port on which to listen for connections.
		@type spawn_thread: The class of the thread to spawn to handle any client
		    connections received by this server.
		@param spawn_args: tuple
		@param spawn_args: The arguments to pass to any spawned threads.
		
		@raise socket.error: If the port could not be bound.
		"""
		threading.Thread.__init__(self)
		self.setDaemon(False)
		
		self._spawn_thread = spawn_thread
		self._spawn_args = spawn_args
		
		self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		self._server.settimeout(10)
		self._server.bind(('0.0.0.0', port))
		self._server.listen(5)
		
	def kill(self):
		"""
		Indicates that this server should terminate gracefully.
		"""
		self._alive = False
		
	def run(self):
		"""
		Continually waits to accept connections from clients, delegating any
		received requests to specialized threads.
		"""
		while self._alive:
			try:
				(client, client_address) = self._server.accept()
				self._spawn_thread(client, self._spawn_args).start()
			except socket.timeout: #Provides an opportunity for shutting down.
				pass
				
		try:
			self._server.shutdown(socket.SHUT_RDWR)
		except:
			pass
		try:
			self._server.close()
		except:
			pass
			
class ClientSocket(object):
	"""
	A class for managing connections from a UTc! client.
	"""
	_client = None #: The connection to the client.
	_data_box = None #: A container for data remaining to be processed.
	
	def __init__(self, client, data_box):
		"""
		Initialises the variables required for managing this connection.
		
		@type client: socket.socket
		@param client: The socket that communicates with the client.
		@type data_box: L{DataBox}
		@param data_box: The DataBox into which all read data will be placed.
		"""
		self._client = client
		self._data_box = data_box
		
	def kill(self):
		"""
		Gracefully terminates this connection.
		"""
		try:
			self._client.shutdown(socket.SHUT_RDWR)
		except:
			pass
		try:
			self._client.close()
		except:
			pass
			
	def readData(self, block=False):
		"""
		Reads data from the connection, storing anything received in
		C{self._data_box}.
		
		@type block: bool
		@param block: If True, this function will raise a TimeoutError if
		    C{_TIMEOUT} seconds pass without any data being received. If False,
		    then this function returns immediately, regardless of whether data
		    was received or not.
		
		@raise L{ConnectionError}: If the connection to the client was
		    interrupted.
		@raise L{TimeoutError}: If no data was received.
		"""
		timeout_counter = 0.0
		client = self._client
		data_box = self._data_box
		while True:
			(input_events, output_events, error_events) = select.select([client], [], [client], 0.01)
			if client in input_events:
				try:
					data = client.recv(_PACKET_SIZE)
					if len(data) == 0:
						raise ConnectionError("Client disconnected")
					data_box.feedData(data)
					return
				except:
					raise ConnectionError("Client disconnected")
			elif client in error_events:
				raise ConnectionError("Client disconnected")
			else:
				if not block or data_box.hasLines():
					return
				if timeout_counter >= _TIMEOUT:
					raise TimeoutError("Timed out while waiting for data")
				timeout_counter += 0.05
				time.sleep(0.05)
				
	def sendData(self, data):
		"""
		Sends data to the client.
		
		@type data: str
		@param data: The line of data to be sent, not newline-terminated.
		
		@raise L{ConnectionError}: If the connection to the client was
		    interrupted.
		"""
		try:
			self._client.sendall(data + "\n")
		except:
			raise ConnectionError("Client disconnected")
			
			
class Error(Exception):
	"""
	The base exception from which all exceptions native to this module inherit.
	"""
	def __init__(self, message):
		"""
		Sets the message for a generic exception.
		"""
		Exception.__init__(self, message)
		
class ConnectionError(Error):
	"""
	Indicates that a problem occurred while communicating with the client.
	"""
	pass
	
class TimeoutError(Error):
	"""
	Indicates that the client failed to provide data within the expected
	timeframe.
	"""
	pass
	